에라토네스 체 구현 부분에 과할 정도로 공을 들인 것만 빼면 평범한 풀이이다.(모든 사람의 풀이를 보진 못해서 확신할 수는 없지만)
풀이를 잘 보면 먼저 1. 에라토네스 체를 구현하고 2. 소수를 찾아 출력하는 식으로 진행된다.
이제 여기에 함수형 패러다임을 적용해보자.
함수형 패러다임
에라토네스 체 구현
Python 에서 함수형으로 에라토네스 체를 구현하는 방법을 검색해보니 그다지 많은 자료가 나오지 않았다.
그래서 순수 함수형 언어인 Haskell 방식을 참고하여 구현해보았다.
Haskell에서 구현된 에라토네스 체는 다음과 같다.
Haskell을 모른다면 굳이 이해할 필요는 없다.(밑에 파이썬으로 설명할테니까)
해당 코드는 재귀를 사용해 다음과 같이 돌아간다.
2부터 시작해서 1씩 세어 나가는 반복자를 sieve 에 넘긴다.
반복자를 받은 sieve 함수는 다음과 같이 작동한다.
[2, 3, 4, 5, 6, ...]를 받는다.
첫번째 인자인 2를 p에 할당하고 나머지 인자인 [3, 4, 5, 6, 7...]를 xs에 할당한다.
p를 반환한다.
xs에서 2의 배수를 제거한 리스트 [3, 5, 7, 9, 11, ...]를 다시 sieve에 넘긴다.
이를 받은 sieve 함수는 다음과 같이 작동한다.
[3, 5, 7, 9, 11, ...]를 받는다.
첫번째 인자인 3을 p에 할당하고 나머지 인자인 [5, 7, 9, 11, 13, ...]를 xs에 할당한다.
이제 이를 이용해 primes 함수를 구현하기 전에 먼저 무한 반복자인 itertools.count 함수에 대해 설명하겠다. itertools.count(start=0, step=1)는 start부터 step씩 증가하는 무한 반복자를 반환한다.
쉽게 말해서 다음과 같은 코드와 같다.
이를 이용해 primes 함수를 구현하면 다음과 같다.
9020번 문제에서는 최댓값이 정해져 있는 소수 리스트가 필요하기 때문에 일정 수 이하의 소수까지만 반복하는 반복자를 추가로 만들어보았다.
이를 위해 itertools.takewhile 함수를 사용했다. itertools.takewhile(predicate, iterable)는 predicate가 True를 반환하는 동안 iterable의 요소를 반환하는 반복자를 반환한다.
다음과 같은 코드와 같다.
예를 들어 list(takewhile(lambda x: x < 10, count(1))) == [1, 2, 3, 4, 5, 6, 7, 8, 9]이다.
여기에 primes 함수를 이용해 특정 수보다 작은 소수를 생성하는 생성자를 다음과 같이 구현할 수 있다.
구현한 함수들을 이용해 소수 판별 함수를 다음과 같이 구현할 수 있다.
본문
이제 본론으로 들어가보자.
원래 코드는 다음과 같다.
먼저 n이 4가 아닌 경우부터 고려하자.
먼저 n을 반으로 나눈 n2를 구하고, n2가 짝수라면 1을 빼줘야한다.
하지만 사실 if 문 쓸 것도 없이 n2 = n // 2 - 1 + (n // 2) % 2로 바로 구할 수 있다.
이제 n2부터 2씩 감소하면서 prime이 소수이고 n - n2도 소수인 n2를 찾으면 된다. zip을 이용해 둘을 짝짓고, filter를 이용해 둘다 소수인 것만 걸러내고, next를 이용해 첫 번째 값을 가져오면 된다.
먼저 둘다 소수인지 판정하는 함수는 is_prime을 이용해 구현할 수 있다.
결과는 200ms로 기존 코드의 결과인 52ms보다 느리다. sieve 함수는 꼬리재귀를 사용하는데, 파이썬에서는 꼬리재귀를 최적화하지 않기 때문에 영향을 미친 것 같다.
하지만 재밌는 과정이었다!
이미 풀어본 문제도 함수형 패러다임을 적용해봐야겠다.
리스트 컴프리헨션과 filter(221219 추가)
8번째 줄의 n for n in nums if n % p != 0를 filter(p.__rmod__, nums)로 변경해보았다. 해당 풀이는 172ms 라는 무려 28ms나 단축된 결과를 얻을 수 있었다!
리스트 컴프리헨션이 filter보다 읽기 더 편한건 사실이지만 아직 성능적으로는 아쉬운 부분이 있는 것 같다.
더 많은 코드를 작성해보며 적재적소에 맞춰 사용하자!